<?php
/**
 * php GEM Project
 * 
 * LICENSE
 * 
 * PHP System Architecture Enabling Rapid Application Deployment and Scalability
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @copyright Copyright (C) 2010 Zeletron LLC (http://zeletron.com)
 * @license http://gem.zeletron.com/license
 */

/*
 * GEM/Model/Domain/File.php
 * 
 * This is the file domain object. Setter methods specific
 * to this class provide a fluent interface.
 * The file domain object serves the purpose of managing files.
 * 
 * This class uses the Fileinfo class, standard with PHP 5.3. 
 * 
 * @author David Joly
 * @link http://zeletron.com
 * 
 * @category GEM
 * @package GEM_Model
 * @subpackage GEM_Model_Domain
 * 
 * @uses Fileinfo
 */
class GEM_Model_Domain_File extends GEM_Model_Domain_Object implements Zend_Acl_Resource_Interface
{	
	/**
	 * The object's resourceId. Override 
	 * the resourceId in extending classes if needed.
	 * 
	 * @var string
	 */
	protected $_resourceId;
	
	/**
	 * The user id of file owner.
	 * 
	 * @var int
	 */
	protected $_owner;
	/**
	 * The file's extension
	 * 
	 * @var string
	 */
	protected $_extension;
	
	/**
	 * The file's mime type
	 * 
	 * @var string
	 */
	protected $_mimeType;
	
	/**
	 * @var string
	 */
	protected $_encoding;
	
	/**
	 * Name of file's directory
	 * 
	 * @var string
	 */
	protected $_dirname;
	
	/**
	 * The file's basename
	 * 
	 * @var string
	 */
	protected $_basename;
	/**
	 * The filename
	 * 
	 * @var string
	 */
	protected $_filename;
	
	/**
	 * The file's size in bytes
	 * 
	 * @var int
	 */
	protected $_filesize;
	
	/**
	 * The title of the file
	 * 
	 * @var string
	 */
	protected $_title;
	
	/**
	 * Description of the File
	 * 
	 * @var string
	 */
	protected $_description;
	
	/**
	 * The file's caption
	 * 
	 * @var string
	 */
	protected $_caption;
	
	/**
	 * @var string
	 */
	protected $_path;
	
	/**
	 * @var string
	 */
	protected $_createdDate;
	
	/**
	 * @var string
	 */
	protected $_modifiedDate;
	
	/**
	 * The original file
	 * 
	 * @var resource
	 */
	protected $_file;
	
	/**
	 * Constructor 
	 * 
	 * @param int $id | optional
	 * @param string $filepath | optional
	 * @return void
	 */
	public function __construct($id = null,$filepath = null){
		parent::__construct($id);
		if(!is_null($filepath))$this->init($filepath);
		
		$this->_safemodeExclude(array(
			'_path', '_dirname','_file'
		));
	}
	
	/**
	 * Initializes the file object by examining the actual file and
	 * setting available properties.
	 * 
	 * @param string $filepath | The path to the file from the SITE_PATH or the full path on the server
	 * @return GEM_Model_Domain_File_File | Enables a fluent interface
	 * @throws GEM_Model_Domain_Exception
	 * @uses Fileinfo
	 */
	public function init($filepath)
	{
		$this->_setPath($filepath);
		$filepath = $this->getPath(true);
		if(!file_exists($filepath))
			throw new GEM_Model_Domain_Exception("No file found at $filepath");
		/**
		 * Using FILEINFO_MIME. FILEINFO_MIME returns both the mime type
		 * and the encoding. Since PHP5.3, the mime type and encoding can
		 * be retrieved separately using FILEINFO_MIME_TYPE and FILE_INFO_ENCODING.
		 * @link http://www.php.net/manual/en/fileinfo.constants.php
		 */
		$finfo = new finfo(FILEINFO_MIME);
		$fileType = $finfo->file($filepath);
		list($this->_mimeType, $this->_encoding) = explode('; charset=',$fileType);
		//Retrieve path info
		list($this->_dirname, $this->_basename, $this->_extension,$this->_filename) = array_values(pathinfo($filepath));
		$this->_filesize = filesize($filepath);	
		$this->clearFile();	
	}
	
	/**
	 * Clears the _file resource property.
	 * When replacing the actual file in an
	 * already-instantiated File object, such
	 * as may be the case in a file upload, clear
	 * file should be called to remove the old
	 * resource handle if it exists.
	 * 
	 * @return void
	 */
	public function clearFile()
	{
		$this->_file = null;
	}
	
	/**
	 * @return GEM_Model_Domain_User
	 */
	public function getOwner()
	{
		return GEM_Model_Domain_User::getFinder('User')->find($this->_owner);
	}
	
	/**
	 * @return string
	 */
	public function getExtension()
	{
		return $this->_extension;
	}
	
	/**
	 * @return string
	 */
	public function getMimeType()
	{
		return $this->_mimeType;
	}
	
	/**
	 * @return string
	 */
	public function getEncoding()
	{
		return $this->_encoding;
	}
	
	/**
	 * @return string
	 */
	public function getFilename()
	{
		return $this->_filename;
	}
	
	/**
	 * @return string
	 */
	public function getDirname()
	{
		return $this->_dirname;
	}
	
	/**
	 * @return string
	 */
	public function getBasename()
	{
		return $this->_basename;
	}
	
	/**
	 * Filesize in bytes
	 * 
	 * @return int
	 */
	public function getFilesize()
	{
		return $this->_filesize;
	}
	
	/**
	 * @return string
	 */
	public function getTitle()
	{
		return $this->_title;
	}
	
	/**
	 * @return string
	 */
	public function getDescription()
	{
		return $this->_description;
	}
	
	/**
	 * @return string
	 */
	public function getCaption()
	{
		return $this->_caption;
	}
	
	/**
	 * Path to file on server
	 * 
	 * @param boolean $fullyQualified = false
	 * @return string
	 */
	public function getPath($fullyQualified = false)
	{
		return $fullyQualified ? SITE_PATH . $this->_path : $this->_path;
	}
	
	/**
	 * Returns the url to the file. If the file is located in a public directory,
	 * the url will point to the file. If the file is located in a private directory
	 * the url will point to the FileController's file action script.
	 * 
	 * By default, the fully qualified url will be returned.
	 * 
	 * $options = array(
	 * 		'fullyQualified' => true
	 * 	)
	 * 
	 * @param array $options
	 * @return string
	 */
	public function getUrl(array $options = array())
	{
		$fullyQualified = array_key_exists('fullyQualified',$options) ? $options['fullyQualified'] : true;
		$url = $fullyQualified ? Zend_Registry::get('config')->base_url : '';
		if($this->isPublic()){
			$url .= substr($this->getPath(true),strlen(PUBLIC_PATH));
		} else {
			$url .= "/file/{$this->_id}/{$this->_basename}";
		}
		//Replace backward slashes if on a Windows machine
		$url = str_replace("\\","/",$url);
		return $url;
	}
	
	/**
	 * @return file resource
	 */
	public function getFile()
	{
		return $this->_file;
	}
	
	/**
	 * @return string
	 */
	public function getResourceId()
	{
		return $this->_resourceId;
	}
	
	/**
	 * @return string
	 */
	public function getCreatedDate()
	{
		return $this->_createdDate;
	}
	
	/**
	 * Returns the modifed date.
	 * 
	 * @return string
	 */
	public function getModifiedDate()
	{
		return $this->_modifiedDate;
	}
	
	/**
	 * Verifies the file has a modifiedDate. Use
	 * this method to verify the file object has
	 * been modified before trying to access the
	 * modifiedDate property.
	 * 
	 * @return boolean
	 */
	public function isModified()
	{
		return !is_null($this->_modifiedDate) ? true : false;
	}
	
	/**
	 * Checks if file is stored in a public directory.
	 * 
	 * @return boolean
	 */
	public function isPublic()
	{
		if(! defined('PUBLIC_PATH'))
			throw new GEM_Model_Domain_Exception('PUBLIC_PATH not defined.');
		if(substr_count($this->getPath(),PUBLIC_PATH) === 1) return true;
		else return false;
	}
	
	/**
	 * Modfies the actual file's extension and changes
	 * the file path to reflect the changes. 
	 * 
	 * @param string $ext
	 * @return boolean 
	 * @throws GEM_Model_Domain_Exception
	 */
	public function changeExtension($ext)
	{
		if(preg_match("/[^a-zA-Z]/",$ext))
			GEM_Model_Domain_Exception("Invalid extension passed to " . __METHOD__ . ". Must be alpha-numeric.");
		if($this->_rename("{$this->_filename}.{$ext}")){
			$this->_extension = $ext;
			return true;
		} else { return false; }
	}
	
	/**
	 * Modifies the actual file's name and changes
	 * the file's path property to reflect the changes.
	 * The method will accept either the filename or 
	 * the basename. 
	 * 
	 * @param string $name
	 * @return boolean 
	 * @throws GEM_Model_Domain_Exception
	 */
	public function rename($name)
	{
		if(preg_match("/\/|\\\/",$name) > 0)
			throw new GEM_Model_Domain_Exception("Invalid filename passed to " . __METHOD__ . ". Basename or filename expected.");
		//Using pathinfo to get the extension in case basename is used.
		$pathparts = pathinfo($name);
		if(array_key_exists('extension',$pathparts)) {
			$basename = $name;
		} else {
			$basename = "{$name}.{$this->_extension}";
		}
		if($this->_rename($basename)){
			$this->_filename = $pathparts['filename'];
			if(array_key_exists('extension',$pathparts)){
				$this->_extension = $pathparts['extension'];
			}
			return true;
		}
		return false;
	}
	
	/**
	 * Relocates the file to the specified directory and
	 * changes the file's path and dirname properties to 
	 * reflect the changes.
	 * 
	 * @param string $dir
	 * @return boolean
	 * @throws GEM_Model_Domain_Exception
	 */
	public function move($dir)
	{
		if(!is_dir($dir))
			throw new GEM_Model_Domain_Exception("$dir is not a valid directory.");
		$oldname = $this->getPath(true);
		$newname = $dir . DIRECTORY_SEPARATOR . $this->_basename;
		if(file_exists($newname))
			throw new GEM_Model_Domain_Exception("A file already exists at $newname. Cannot move file.");
		if(rename($oldname, $newname)){
			$this->_setPath($newname);
			$this->_dirname = $dir;
			return true;
		} else { return false; }
	}

	/**
	 * @param string $resourceId
	 * @return GEM_Model_Domain_File | Enables a Fluent Interface
	 */
	public function setResourceId($resourceId)
	{
		if(!is_null($this->_resourceId))$this->_markPropertyAsChanged('resourceId');
		$this->_resourceId = $resourceId;
		return $this;
	}
	
	/** 
	 * @param string $title
	 * @return GEM_Model_Domain_File | Enables a Fluent Interface
	 */
	public function setTitle($title)
	{
		if(!is_null($this->_title))$this->_markPropertyAsChanged('title');
		$this->_title = $title;
		return $this;
	}
	
	/** 
	 * @param string $description
	 * @return GEM_Model_Domain_File | Enables a Fluent Interface
	 */
	public function setDescription($description)
	{
		if(!is_null($this->_description))$this->_markPropertyAsChanged('description');
		$this->_description = $description;
		return $this;
	}
	
	/**
	 * @param int|GEM_Model_Domain_User $user The user id or object
	 * @return GEM_Model_Domain_User | Enables a Fluent Interface
	 * @throws GEM_Model_Domain_Exception
	 */
	public function setOwner($user)
	{
		if(!is_null($this->_owner)) $this->_markPropertyAsChanged('owner');
		if($user instanceof GEM_Model_Domain_User) { 
			$this->_owner = $user->getId(); 
		} else if(is_numeric($user)) { 
			$this->_owner = (int)$user; 
		} else { 
			throw new GEM_Model_Domain_Exception(__METHOD__ . " expects either a numerical id value or a GEM_Model_Domain_User object.");
		}
		return $this;
	}
	
	/** 
	 * @param string $caption
	 * @return GEM_Model_Domain_File | Enables a Fluent Interface
	 */
	public function setCaption($caption)
	{
		if(!is_null($this->_caption))$this->_markPropertyAsChanged('caption');
		$this->_caption = $caption;
		return $this;
	}
	
	/** 
	 * @param string $date
	 * @return GEM_Model_Domain_File | Enables a Fluent Interface
	 * @uses Zend_Date
	 * @throws GEM_Model_Domain_Exception
	 */
	public function setCreatedDate($date)
	{
		if(!Zend_Date::isDate($date,'Y-m-d H:i:s'))
			throw new GEM_Model_Domain_Exception('Invalid date value.');
		if(isset($this->_createdDate)) $this->_markPropertyAsChanged('createdDate');
		$this->_createdDate = $date;
		return $this;
	}
	
	/** 
	 * @param string $date
	 * @return GEM_Model_Domain_File | Enables a Fluent Interface
	 * @uses Zend_Date
	 * @throws GEM_Model_Domain_Exception
	 */
	public function setModifiedDate($date)
	{
		if(!Zend_Date::isDate($date,'Y-m-d H:i:s') && $date != '0000-00-00 00:00:00')
			throw new GEM_Model_Domain_Exception('Invalid date value.');
		if(isset($this->_modifiedDate)) $this->_markPropertyAsChanged('modifiedDate');
		$this->_modifiedDate = $date;
		return $this;
	}
	
	/**
	 * @param string $path
	 * @return void
	 * @throws GEM_Model_Domain_Exception
	 */
	protected function _setPath($path)
	{
		if(!defined('SITE_PATH'))
			throw new GEM_Model_Domain_Exception('SITE_PATH not defined.');
    	$path = str_replace(SITE_PATH,'',$path);
		$path = str_replace('/',DIRECTORY_SEPARATOR,$path);
		$path = str_replace('\\',DIRECTORY_SEPARATOR,$path);
		if($this->_path != $path && $this->_path !== null) $this->_markPropertyAsChanged('path');
		$this->_path = $path;
	}
	
	/**
	 * Renames the file.
	 * 
	 * @param string $basename
	 * @return boolean
	 */
	protected function _rename($basename)
	{
		$oldname = $this->getPath(true);
		$newname = $this->_dirname . DIRECTORY_SEPARATOR . $basename;
		if(file_exists($newname))
			throw new GEM_Model_Domain_Exception("The file $basename already exists at {$this->_dirname}. Cannot rename file.");
		if(!rename($oldname,$newname)) return false;
		$this->_setPath($newname);
		$this->_basename = $basename;
		return true;
	}
	
	/**
	 * Prepares the file object for serialization.
	 * 
	 * @return void
	 */
	public function __sleep()
	{
		$this->clearFile();
		return array_keys(get_object_vars($this));
	}
}